package jbitarrays;

import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.NotThreadSafe;

import jbitarrays.api.InfiniteBitArray;

/**
 * An {@link InfiniteBitArray} backed by a {@link File}
 */
@NotThreadSafe final class InfiniteBitFile implements InfiniteBitArray, Closeable {
	private final @Nonnull FileChannel file;
	/**
	 * the size of the current {@link #map}
	 */
	private final int bufferSizeBytes;
	private final int overlapBytes;

	private int mapStartIndexBytes;
	private @Nonnull MappedByteBuffer map;

	public InfiniteBitFile(@Nonnull File file, @Nonnegative int bufferSize, @Nonnegative int overlap) {
		if (file == null || !file.exists() || !file.isFile() || !file.canRead() || !file.canWrite()) {
			throw new IllegalArgumentException("must be a valid readable file");
		}
		try {
			this.file = new RandomAccessFile(file, "rw").getChannel();
		} catch (FileNotFoundException e) {
			throw new RuntimeException(e);
		}

		if (bufferSize < Integer.SIZE) {
			throw new IllegalArgumentException("buffer size must be an int or bigger or implementation won't work");
		}
		this.bufferSizeBytes = bufferSize;

		if (overlap < 0) {
			throw new IllegalArgumentException("no negative overlap");
		}
		this.overlapBytes = overlap;

		// initialise the map
		reMap(0);
	}

	private void reMap(int newStartIndexBytes) {
		try {
			this.map = this.file.map(MapMode.READ_WRITE, newStartIndexBytes, bufferSizeBytes);
			mapStartIndexBytes = newStartIndexBytes;
		} catch (IOException e) {
			throw new RuntimeException(e);
		}
	}

	private void ensureIndexMapped(int indexBits, int howManyBits) {
		int startIndexBytes = indexBits / Byte.SIZE;
		int endIndexBytes = startIndexBytes + (howManyBits / Byte.SIZE) + 1;
		if (startIndexBytes <= mapStartIndexBytes) {
			// shift down
			int downOneSize = mapStartIndexBytes + overlapBytes - bufferSizeBytes;
			int definitelyOK = startIndexBytes - overlapBytes;
			int newStartIndexBytes;
			if (downOneSize + bufferSizeBytes >= endIndexBytes && downOneSize <= startIndexBytes) {
				// downOneSize contains start & end
				newStartIndexBytes = Math.max(0, downOneSize);
			} else {
				newStartIndexBytes = Math.max(0, definitelyOK);
			}
			if (mapStartIndexBytes != newStartIndexBytes) {
				reMap(newStartIndexBytes);
			}
		} else if (endIndexBytes >= mapStartIndexBytes + bufferSizeBytes) {
			// shift up
			int definitelyOK = endIndexBytes + overlapBytes - bufferSizeBytes;
			int upOneSize = mapStartIndexBytes + bufferSizeBytes - overlapBytes;
			int newStartIndexBytes;
			if (upOneSize + bufferSizeBytes >= endIndexBytes && upOneSize <= startIndexBytes) {
				// downOneSize contains start & end
				newStartIndexBytes = Math.max(0, upOneSize);
			} else {
				newStartIndexBytes = Math.max(0, definitelyOK);
			}
			if (mapStartIndexBytes != newStartIndexBytes) {
				reMap(newStartIndexBytes);
			}
		}
	}

	public boolean get(int index) throws IllegalArgumentException {
		Utils.checkIndex(index);
		ensureIndexMapped(index, 1);
		return (map.get(byteIndex(index)) & mask(index)) > 0;
	}

	/**
	 * index into map of the byte containing this index
	 */
	private int byteIndex(int indexBits) {
		return (indexBits / Byte.SIZE) - mapStartIndexBytes;
	}

	/**
	 * a one at the {@link #bitIndex(int)}'th bit of the lowest {@link Byte}
	 */
	private int mask(int index) {
		return 1 << (Byte.SIZE - bitIndex(index) - 1);
	}

	/**
	 * position in byte of this index
	 */
	private int bitIndex(int index) {
		return index & (Byte.SIZE - 1);
	}

	public void set(int index, boolean value) throws IllegalArgumentException {
		Utils.checkIndex(index);
		ensureIndexMapped(index, 1);
		int byteIndex = byteIndex(index);
		byte b = map.get(byteIndex);
		if (value) {
			b |= mask(index);
		} else {
			b &= ~mask(index);
		}
		map.put(byteIndex, b);
	}

	public int getN(int index, int howMany) throws IllegalArgumentException {
		//System.out.printf("get %d %d\n", index, howMany);
		Utils.checkIndex(index);
		Utils.checkHowMany(howMany);

		if (howMany == 0) {
			// trivial case, not really a read
			return 0;
		}
		ensureIndexMapped(index, howMany);

		// the first byte (we might not start at the zero'th bit of it)
		int firstByteIndex = byteIndex(index);
		int bitIndex = bitIndex(index);
		int bitsFromThisByte = Math.min(howMany, Byte.SIZE - bitIndex);
		byte first = map.get(firstByteIndex);
		first >>>= Byte.SIZE - bitIndex - bitsFromThisByte; // chop off unneeded bits to the right, leaving value in RHS of the byte
		first &= (1 << bitsFromThisByte) - 1; // chop off unneeded bits to the left
		howMany -= bitsFromThisByte;
		int firstAsInt = bottom8BitsOfInt(first);
		int ret = firstAsInt << howMany;
		if (howMany == 0) {
			return ret;
		}

		// read whole bytes (as we start from the zero'th bit of each byte now)
		for (int byteIndex = firstByteIndex + 1; howMany > 0; ++byteIndex) {
			int b = bottom8BitsOfInt(map.get(byteIndex));
			bitsFromThisByte = Math.min(howMany, Byte.SIZE);
			howMany -= bitsFromThisByte;
			b >>= Byte.SIZE - bitsFromThisByte; // only the needed number of bits		
			b <<= howMany; // shift into position in ret
			ret |= b;
		}

		return ret;
	}

	public void setN(int index, int howMany, int value) throws IllegalArgumentException {
		//System.out.printf("set %d %d %d\n", index, howMany, value);
		Utils.checkIndex(index);
		Utils.checkHowMany(howMany);

		ensureIndexMapped(index, howMany);

		// THE FIRST BYTE (we might not start at the zero'th bit of it)

		int firstByteIndex = byteIndex(index);
		int bitIndex = bitIndex(index);
		int bitsFromThisByte = Math.min(howMany, Byte.SIZE - bitIndex);

		byte first; // the byte we will eventually write
		byte ones; // a mask, with a 1 in all the bits we will write
		if (bitsFromThisByte == Byte.SIZE && bitIndex == 0) {
			// a common case: we'll overwrite this byte entirely
			first = 0;
			ones = -1;
		} else {
			first = map.get(firstByteIndex);
			// we want to zero out the bits we'll write to
			ones = 1;
			ones <<= bitsFromThisByte;
			ones -= 1;
			ones <<= Byte.SIZE - bitIndex - bitsFromThisByte;
			first &= ~ones; // and flatten these bits
		}

		// prepare the part of the value we're going to add to "first"
		int bitsToWrite = value >>> (howMany - bitsFromThisByte); // byte with value in the lowest "bitsFromThisByte" bits
		bitsToWrite <<= Byte.SIZE - bitIndex - bitsFromThisByte; // byte with value in highest "bitsFromThisByte" of the lowest 8 bits of "bitsToWrite"

		// as bytes are signed in Java, we'll have to do something cunning:
		first |= makeByteOfBottom8Bits(bitsToWrite) & ones;

		// and the write:
		map.put(firstByteIndex, first);
		if (bitsFromThisByte == howMany) {
			// only one byte in this operation, so return
			return;
		}
		howMany -= bitsFromThisByte;

		// READ WHOLE BYTES (as we start from the zero'th bit of each byte now)
		for (int byteIndex = firstByteIndex + 1; howMany > 0; ++byteIndex) {
			bitsFromThisByte = Math.min(howMany, Byte.SIZE);

			// add any existing bits we need into "b"
			byte b;
			if (bitsFromThisByte == Byte.SIZE) {
				// we can ignore the contents of this byte as we'll overwrite it all
				b = 0;
			} else {
				b = map.get(byteIndex);
				b &= (1 << (Byte.SIZE - bitsFromThisByte)) - 1; // ensure we only have the existing bits we want
			}

			// add the next bit of "value" into b
			bitsToWrite = value >>> (howMany - bitsFromThisByte); // byte with value in the lowest "bitsFromThisByte" bits
			bitsToWrite <<= Byte.SIZE - bitsFromThisByte; // byte with value in highest "bitsFromThisByte" of the lowest 8 bits of "bitsToWrite"
			b |= makeByteOfBottom8Bits(bitsToWrite);

			map.put(byteIndex, b);
			howMany -= bitsFromThisByte;
		}
	}

	private byte makeByteOfBottom8Bits(int bits) {
		byte ret = (byte) (bits & 0x7F); // unsigned
		if ((bits & 0x80) > 0) {
			// we have to overwrite first's sign bit
			ret |= Byte.MIN_VALUE;
		}
		return ret;
	}

	private int bottom8BitsOfInt(byte bits) {
		int ret = bits & 0x7F; // unsigned
		if (bits < 0) { // we have to incorporate the sign bit
			ret |= 0x80;
		}
		return ret;
	}

	public void close() throws IOException {
		file.close();
	}

}